package com.easy.mongodb.core.conditions;

import com.easy.mongodb.common.enums.FieldType;
import com.easy.mongodb.common.enums.Keyword;
import com.easy.mongodb.common.enums.Like;
import com.easy.mongodb.common.utils.ArrayUtils;
import com.easy.mongodb.common.utils.CollectionUtils;
import com.easy.mongodb.common.utils.StringUtils;
import com.easy.mongodb.core.biz.DistanceUnit;
import com.easy.mongodb.core.biz.GeoPoint;
import com.easy.mongodb.core.biz.ShapeRelation;
import com.easy.mongodb.core.biz.TableFieldInfo;
import com.easy.mongodb.core.cache.BaseCache;
import com.easy.mongodb.core.conditions.interfaces.Compare;
import com.easy.mongodb.core.conditions.interfaces.Func;
import com.easy.mongodb.core.conditions.interfaces.Geo;
import com.easy.mongodb.core.conditions.interfaces.Nested;
import com.easy.mongodb.core.toolkit.DocumentKit;
import com.easy.mongodb.core.toolkit.FieldUtils;
import com.easy.mongodb.core.toolkit.SerializedLambda;
import com.easy.mongodb.core.toolkit.TableInfoHelper;
import com.mongodb.BasicDBObject;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Projections;
import com.mongodb.client.model.Sorts;
import com.mongodb.client.model.geojson.Geometry;
import lombok.Getter;
import lombok.Setter;
import org.bson.BsonDocument;
import org.bson.BsonObjectId;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static com.easy.mongodb.common.constants.BaseMongoConstants.DEFAULT_ID_NAME;
import static com.easy.mongodb.common.constants.BaseMongoConstants.DEFAULT_MONGO_ID_NAME;
import static com.easy.mongodb.common.enums.Keyword.*;

/**
 * ProjectName: easy-mongodb
 * Description: 查询条件封装
 * Author: vapeshop
 * Date: 2022/7/12 15:18:52
 * UpdateUser: vapeshop
 * UpdateDate: 2022/7/12 15:18:52
 * UpdateRemark: The modified content
 * Version: 1.0
 * <p>
 * Copyright © 2022 vapeshop Technologies Inc. All Rights Reserved
 **/
@SuppressWarnings({"unchecked"})
public abstract class AbstractWrapper<T, R, Children extends AbstractWrapper<T, R, Children>> extends Wrapper<T>
        implements Compare<Children, R>, Nested<Children, Children>, Func<Children, R>, Geo<Children, R> {

    /**
     * 占位符
     */
    protected final Children typedThis = (Children) this;
    /** mongo执行条件 */
    protected Bson conditions;
    @Setter
    @Getter
    private String collectionName;
    //    @Setter
//    @Getter
//    private List<Document> documents = new ArrayList<Document>();
    @Setter
    @Getter
    private List<Bson> query = new ArrayList<Bson>();
    @Setter
    @Getter
    private List<Bson> data = new ArrayList<Bson>();
    /** mongo执行排序 */
    @Setter
    @Getter
    private Bson sort;
    /** mongo执行过滤条件 */
    private List<Bson> projection = new ArrayList<Bson>();
    @Setter
    @Getter
    private int limit = 0;
    @Setter
    @Getter
    private int skip = 0;
    /**
     * 数据库表映射实体类
     */
    private T entity;
    /**
     * 实体类型(主要用于确定泛型以及取TableInfo缓存)
     */
    private Class<T> entityClass;

    @Override
    public Bson getConditions() {
        return this.query.size() == 0 ? new BsonDocument() : Filters.and((Iterable) this.query);
    }

    @Override
    public Bson getProjection() {
        return this.projection.size() == 0 ? new BsonDocument() : Projections.fields(this.projection);
    }

    public void setProjection(Bson bson) {
        this.projection.add(bson);
    }

    @Override
    public T getEntity() {
        return this.entity;
    }

    public Children setEntity(T entity) {
        this.entity = entity;

        return this.typedThis;
    }

    public Class<T> getEntityClass() {
        if (this.entityClass == null && this.entity != null) {
            this.entityClass = (Class<T>) this.entity.getClass();
        }
        return this.entityClass;
    }

    public Children setEntityClass(Class<T> entityClass) {
        if (entityClass != null) {
            this.entityClass = entityClass;
        }
        return this.typedThis;
    }

    @Override
    public <V> Children allEq(boolean condition, Map<R, V> params, boolean null2IsNull) {
        if (condition && CollectionUtils.isNotEmpty(params)) {
            params.forEach((k, v) -> {
                if (StringUtils.checkValNotNull(v)) {
                    eq(k, v);
                } else {
                    if (null2IsNull) {
                        isNull(k);
                    }
                }
            });
        }
        return this.typedThis;
    }

    @Override
    public <V> Children allEq(boolean condition, BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull) {
        if (condition && CollectionUtils.isNotEmpty(params)) {
            params.forEach((k, v) -> {
                if (filter.test(k, v)) {
                    if (StringUtils.checkValNotNull(v)) {
                        eq(k, v);
                    } else {
                        if (null2IsNull) {
                            isNull(k);
                        }
                    }
                }
            });
        }
        return this.typedThis;
    }

    @Override
    public Children eq(boolean condition, R column, Object val) {
        return addCondition(condition, column, EQ, val);
    }

    @Override
    public Children ne(boolean condition, R column, Object val) {
        return addCondition(condition, column, NE, val);
    }

    @Override
    public Children gt(boolean condition, R column, Object val) {
        return addCondition(condition, column, GT, val);
    }

    @Override
    public Children ge(boolean condition, R column, Object val) {
        return addCondition(condition, column, GE, val);
    }

    @Override
    public Children lt(boolean condition, R column, Object val) {
        return addCondition(condition, column, LT, val);
    }

    @Override
    public Children le(boolean condition, R column, Object val) {
        return addCondition(condition, column, LE, val);
    }

    @Override
    public Children like(boolean condition, R column, Object val) {
        return likeValue(condition, LIKE, column, val, Like.DEFAULT);
    }

    @Override
    public Children notLike(boolean condition, R column, Object val) {
        return likeValue(condition, NOT_LIKE, column, val, Like.DEFAULT);
    }

    @Override
    public Children likeLeft(boolean condition, R column, Object val) {
        return likeValue(condition, LIKE, column, val, Like.LEFT);
    }

    @Override
    public Children likeRight(boolean condition, R column, Object val) {
        return likeValue(condition, LIKE, column, val, Like.RIGHT);
    }

    @Override
    public Children between(boolean condition, R column, Object val1, Object val2) {
        return maybeDo(condition, () -> {
            getQuery().add(new BasicDBObject().append(columnToString(column), new BasicDBObject("$gte", columnForVal(column, val1)).append("$lte", columnForVal(column, val2))));
        });
    }

    @Override
    public Children notBetween(boolean condition, R column, Object val1, Object val2) {
        return maybeDo(condition, () -> {
            getQuery().add(new BasicDBObject().append(columnToString(column), new BasicDBObject("$lt", columnForVal(column, val1)).append("$gt", columnForVal(column, val2))));
        });
    }

    @Override
    public Children and(boolean condition, Consumer<Children> consumer) {
        return maybeDo(condition, () -> {
            final Children instance = instance();
            consumer.accept(instance);
            getQuery().add(Filters.and(instance.getQuery()));
        });
    }

    @Override
    public Children or(boolean condition, Consumer<Children> consumer) {
        return maybeDo(condition, () -> {
            final Children instance = instance();
            consumer.accept(instance);
            getQuery().add(Filters.or(instance.getQuery()));
        });
    }

    @Override
    public Children nested(boolean condition, Consumer<Children> consumer) {
        return maybeDo(condition, () -> {
            final Children instance = instance();
            consumer.accept(instance);
            getQuery().add(Filters.and(instance.getQuery()));
        });
    }

    @Override
    public Children not(boolean condition, Consumer<Children> consumer) {
        return maybeDo(condition, () -> {
            final Children instance = instance();
            consumer.accept(instance);
            getQuery().add(Filters.not(Filters.and(instance.getQuery())));
        });
    }

    @Override
    public Children isNull(boolean condition, R column) {
        return maybeDo(condition, () -> {
            getQuery().add(new BasicDBObject().append(columnToString(column), null));
        });
    }

    @Override
    public Children isNotNull(boolean condition, R column) {
        return maybeDo(condition, () -> {
            getQuery().add(new BasicDBObject().append(columnToString(column), new BasicDBObject("$ne", null)));
        });
    }

    @Override
    public Children in(boolean condition, R column, Collection<?> values) {
        return maybeDo(condition, () -> {
            if (DEFAULT_MONGO_ID_NAME.equals(columnToString(column))) {
                getQuery().add(Filters.in(columnToString(column), values));
            } else {
                getQuery().add(Filters.in(columnToString(column), values.stream().map(e -> columnForVal(column, e)).collect(Collectors.toList())));
            }
        });
    }

    @Override
    public Children in(boolean condition, R column, Object... values) {
        return in(condition, column, Arrays.asList(values));
    }

    @Override
    public Children notIn(boolean condition, R column, Collection<?> values) {
        return maybeDo(condition, () -> {
            if (DEFAULT_MONGO_ID_NAME.equals(columnToString(column))) {
                List<ObjectId> idList = new ArrayList<ObjectId>();

                Iterator iter = values.iterator();
                while (iter.hasNext()) {
                    Object value = (Object) iter.next();
                    idList.add(new ObjectId(String.valueOf(value)));
                }
                getQuery().add(Filters.nin(columnToString(column), idList));
            } else {
                getQuery().add(Filters.nin(columnToString(column), values));
            }
        });
    }

    @Override
    public Children notIn(boolean condition, R column, Object... values) {
        return notIn(condition, column, Arrays.asList(values));
    }

    @Override
    public Children nestedMatch(boolean condition, String path, String column, Object val) {
        return maybeDo(condition, () -> {
            getQuery().add(Filters.eq(path + "." + column, val));
        });
    }

    @Override
    public Children geoBoundingBox(boolean condition, String column, GeoPoint topLeft, GeoPoint bottomRight) {
        return maybeDo(condition, () -> {
            getQuery().add(Filters.geoWithinBox(column, topLeft.getX(), topLeft.getY(), bottomRight.getX(), bottomRight.getY()));
        });
    }

    @Override
    public Children geoDistance(boolean condition, String column, Double distance, DistanceUnit distanceUnit, GeoPoint centralGeoPoint) {
        return maybeDo(condition, () -> {
            getQuery().add(Filters.geoWithinCenter(column, centralGeoPoint.getX(), centralGeoPoint.getY(), DistanceUnit.convert(distance, distanceUnit, DistanceUnit.KILOMETERS)));
        });
    }

    @Override
    public Children geoDistance(boolean condition, String column, String distance, GeoPoint centralGeoPoint) {
        return maybeDo(condition, () -> {
            getQuery().add(Filters.geoWithinCenter(column, centralGeoPoint.getX(), centralGeoPoint.getY(), DistanceUnit.parse(distance, DistanceUnit.KILOMETERS, DistanceUnit.METERS)));
        });
    }

    @Override
    public Children geoPolygon(boolean condition, String column, List<GeoPoint> geoPoints) {
        return maybeDo(condition, () -> {
            List<List<Double>> points = new ArrayList<>(geoPoints.size());
            geoPoints.stream().forEach(e -> {
                List<Double> point = new ArrayList<>(2);
                point.add(e.getX());
                point.add(e.getY());
                points.add(point);
            });
            getQuery().add(Filters.geoWithinPolygon(column, points));
        });
    }


    @Override
    public Children geoShape(boolean condition, String column, Geometry geometry, ShapeRelation shapeRelation) {
        return maybeDo(condition, () -> {
            switch (shapeRelation) {
                case INCLUSION:
                    this.getQuery().add(Filters.geoWithin(column, geometry));
                    break;
                case PROXIMITY:
//                    this.getQuery().add(Filters.near(column, geometry));
                    break;
                case INTERSECTION:
                    this.getQuery().add(Filters.geoIntersects(column, geometry));
                    break;
                default:
                    break;
            }
        });
    }

    @Override
    public Children orderBy(boolean condition, boolean isAsc, R column, R... columns) {
        return maybeDo(condition, () -> {
            Bson sort2 = null;
            if (ArrayUtils.isNotEmpty(columns)) {
                List<String> fields = new ArrayList<>();
                if (!Objects.isNull(column)) {
                    fields.add(columnToString(column));
                }
                Arrays.stream(columns).map(c -> columnToString(c)).forEach(fields::add);
                sort2 = isAsc ? Sorts.ascending(fields) : Sorts.descending(fields);
            }
            if (this.sort == null) {
                this.sort = sort2;
            } else {
                this.sort = Sorts.orderBy(this.sort, sort2);
            }
        });
    }

    @Override
    public Children orderBy(boolean condition, boolean isAsc, R column) {
        return maybeDo(condition, () -> {
            Bson sort2 = isAsc ? Sorts.ascending(columnToString(column)) : Sorts.descending(columnToString(column));
            if (this.sort == null) {
                this.sort = sort2;
            } else {
                this.sort = Sorts.orderBy(this.sort, sort2);
            }

        });
    }

    @Override
    public Children func(boolean condition, Consumer<Children> consumer) {
        return maybeDo(condition, () -> consumer.accept(this.typedThis));
    }

    /**
     * 内部自用
     * <p>拼接 LIKE 以及 值</p>
     */
    protected Children likeValue(boolean condition, Keyword keyword, R column, Object value, Like sqlLike) {
        return maybeDo(condition, () -> {
            Pattern pattern = null;
            //NOT_LIKE "^((?!" + "关键字" + ").)*$'
            switch (sqlLike) {
                case LEFT:
                    if (NOT_LIKE.equals(keyword)) {
                        pattern = Pattern.compile("^((?!" + value + ".*$", Pattern.CASE_INSENSITIVE);
                    } else {
                        pattern = Pattern.compile("^" + value + ".*$", Pattern.CASE_INSENSITIVE);
                    }
                    break;
                case RIGHT:
                    if (NOT_LIKE.equals(keyword)) {
                        pattern = Pattern.compile("^.*((?!" + value + "$", Pattern.CASE_INSENSITIVE);
                    } else {
                        pattern = Pattern.compile("^.*" + value + "$", Pattern.CASE_INSENSITIVE);
                    }
                    break;
                default:
                    if (NOT_LIKE.equals(keyword)) {
                        pattern = Pattern.compile("^.*((?!" + value + ".*$", Pattern.CASE_INSENSITIVE);
                    } else {
                        pattern = Pattern.compile("^.*" + value + ".*$", Pattern.CASE_INSENSITIVE);
                    }
            }
            getQuery().add(Filters.regex(columnToString(column), pattern));

        });
    }

    /**
     * 普通查询条件
     *
     * @param condition  是否执行
     * @param column     属性
     * @param sqlKeyword SQL 关键词
     * @param val        条件值
     */
    protected Children addCondition(boolean condition, R column, Keyword sqlKeyword, Object val) {
        return maybeDo(condition, () -> {
            switch (sqlKeyword) {
                case EQ:
                    getQuery().add(Filters.eq(columnToString(column), columnForVal(column, val)));
                    break;
                case LT:
                    getQuery().add(Filters.lt(columnToString(column), columnForVal(column, val)));
                    break;
                case LE:
                    getQuery().add(Filters.lte(columnToString(column), columnForVal(column, val)));
                    break;
                case GT:
                    getQuery().add(Filters.gt(columnToString(column), columnForVal(column, val)));
                    break;
                case GE:
                    getQuery().add(Filters.gte(columnToString(column), columnForVal(column, val)));
                    break;
            }
        });
    }

    /**
     * 多重嵌套查询条件
     *
     * @param condition 查询条件值
     */
    protected Children addNestedCondition(boolean condition, Consumer<Children> consumer) {
        return maybeDo(condition, () -> {
            final Children instance = instance();
            consumer.accept(instance);
//            appendSqlSegments(APPLY, instance);
        });
    }

    /**
     * 子类返回一个自己的新对象
     */
    protected abstract Children instance();


    /**
     * 函数化的做事
     *
     * @param condition 做不做
     * @param something 做什么
     * @return Children
     */
    protected final Children maybeDo(boolean condition, DoSomething something) {
        if (condition) {
            something.doIt();
        }
        return this.typedThis;
    }


    /**
     * 必要的初始化
     */
    protected void initNeed() {
        this.query = new ArrayList<Bson>();
        this.data = new ArrayList<Bson>();
    }

    @Override
    public void clear() {
        this.entity = null;
        getQuery().clear();
        this.data.clear();
    }


    /**
     * 获取 columnName
     */
    protected String columnToString(R column) {
        if (this.entityClass == null) {
            this.setEntityClass((Class<T>) SerializedLambda.extract((Serializable) column).getInstantiatedClass());
        }
        return TableInfoHelper.getTableInfo(this.entityClass).getMappingColumn(FieldUtils.getFieldName(column));
    }

    protected Object columnForVal(R column, Object val) {
        String columnName = FieldUtils.getFieldNameNotConvertId(column);
        if (DEFAULT_ID_NAME.equals(columnName)) {
            return new BsonObjectId(new ObjectId(val.toString()));
        } else {
            TableFieldInfo fieldInfo = TableInfoHelper.getTableInfo(this.entityClass).getFieldList().stream().filter(e -> e.getColumn().equalsIgnoreCase(columnName)).findFirst().orElse(null);
            if (fieldInfo != null) {
                if (FieldType.OBJECT_ID.equals(fieldInfo.getFieldType())) {
                    return DocumentKit.toPrimitive(val.toString(), ObjectId.class);
                } else if (FieldType.DATE.equals(fieldInfo.getFieldType())) {
                    if (Date.class.isAssignableFrom(val.getClass())) {
                        return val;
                    } else if (String.class.isAssignableFrom(val.getClass())) {
                        LocalDateTime ld = LocalDateTime.parse(val.toString(),
                                DateTimeFormatter.ofPattern(fieldInfo.getDateFormat()));
                        val = Date.from(ld.atZone(ZoneId.systemDefault()).toInstant());
                        return val;
                    } else if (Long.class.isAssignableFrom(val.getClass())) {
                        return new Date(Long.valueOf(val.toString()));
                    }
                    return DocumentKit.toPrimitive(val.toString(), Date.class);
                }
            }
            return DocumentKit.toPrimitive(val.toString(), BaseCache.getterMethod(this.entityClass, FieldUtils.getFieldName(column)).getReturnType());
        }

    }

    /**
     * 做事函数
     */
    @FunctionalInterface
    public interface DoSomething {

        void doIt();
    }
}
